package com.shanzhe.eshop.common.hdfs.nameNode.server;

import lombok.Data;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * @Author yinwei
 * @create 2021/10/10
 * @Version 1.0
 * @desc 负责管理edits log日志的核心组件
 */
public class FsEditsLog {

    /**
     * 当前递增的txIdSeg序号
     */
    private Long txIdSeg = 0L;

    /**
     * 内存缓冲区
     */
    private DoubleBuffer editsDoubleBuffer = new DoubleBuffer();

    /**
     * 当前是否将内存缓冲刷入磁盘
     */
    private volatile Boolean isSyncRunning = false;

    /**
     * 在同步磁盘中最大的txId
     */
    private volatile Long syncMaxTxId = 0L;

    /**
     * 每个线程自己txId的副本
     */
    private ThreadLocal<Long> localTxId = new ThreadLocal<Long>();

    /**
     * 当前是否有线程在等待下一批syncBuffer刷入到磁盘
     */
    private volatile Boolean isWaitSync = false;


    /**
     * 记录edits log
     * @param content 日志
     */
    public void logEdits(String content){
        //直接加锁
        synchronized (this){
            //获取全局唯一的txId,代表了edits log的序号
            txIdSeg++;
            Long txId = txIdSeg;

            localTxId.set(txId);

            Log log = new Log(txId, content);
            //将edits log写入内存缓冲中
            editsDoubleBuffer.write(log);
        }

        logSync();
    }

    /**
     * 将内存缓冲区数据刷入到磁盘文件中
     * 在这里尝试允许某一个线程一次性将内存缓冲中的数据刷入到磁盘文件中
     * 相当于实现一个批量将内存缓冲数据输入磁盘的过程s
     */
    public void logSync(){
        try {
            //再次尝试加锁
            synchronized (this){
                //如果当前正好有人在刷内存缓冲到磁盘中去
                if(isSyncRunning){
                    //假如说某个线程已经把txId = 1,2,3,4,5的edits log都从syncBuffer刷入到磁盘了
                    //或者说此时正在刷入磁盘
                    //此时syncMaxTxId = 5 代表的是正在刷入磁盘的最大txId
                    //那么这个时候来了一个线程，它对应的txId = 3,此时就可以直接返回
                    //就代表说它对应的edits log肯定已经刷入到磁盘
                    Long txId = localTxId.get();
                    if(txId<=syncMaxTxId){
                        return;
                    }

                    //假如说来了一个txId = 6的线程，那么他就需要等待，同时释放掉锁
                    if(isWaitSync){
                        return;
                    }
                    isWaitSync = true;
                    while (isSyncRunning){
                        wait(2000);
                    }
                    isWaitSync = false;

                }
                //交换两块缓冲区
                editsDoubleBuffer.setReadyToSyn();
                //然后可以保存下当前要同步到磁盘文件中的最大txId
                syncMaxTxId = editsDoubleBuffer.getMaxTxId();
                //设置当前正在同步磁盘的标志位
                isSyncRunning = true;
            }
            //开始同步内存缓冲到内存磁盘文件里去
            //这个过程是比较慢的，基本上是毫秒级的 具体时间不好说因为是写磁盘的操作
            editsDoubleBuffer.flush();

            synchronized (this){
                isSyncRunning = false;
                notifyAll();
            }

        }catch (Exception e){
            e.printStackTrace();
        }


    }


    /**
     * 代表了一条edits log
     */
    @Data
    class Log{

        Long txId;

        String content;

        Log(Long txId,String content){
            this.txId = txId;
            this.content = content;
        }
    }


    /**
     * 内存双缓冲
     */
    class DoubleBuffer{

        /**
         * 是专门用来承载线程写入edits log的
         */
        LinkedList<Log> currentBuffer = new LinkedList<Log>();

        /**
         * 是专门将数据同步到磁盘中去的
         */
        LinkedList<Log> synBuffer = new LinkedList<Log>();

        /**
         * 将edits log写到内存缓冲
         * @param log
         */
        public void write(Log log){
            currentBuffer.add(log);
        }

        /**
         * 交换两个缓冲区为了同步内存数据到磁盘做准备
         */
        public void setReadyToSyn(){
            LinkedList<Log> tmp = currentBuffer;
            currentBuffer = synBuffer;
            synBuffer = tmp;
        }


        /**
         * 获取synBuffer缓冲区最大的一个txId
         * @return
         */
        public Long getMaxTxId(){
            return synBuffer.getLast().getTxId();
        }


        /**
         * 将synBuffer中的数据刷入到磁盘
         */
        public void flush(){
            for(Log log:synBuffer){
                //这里正常来说，就是用文件输出流将数据写入到磁盘文件中
                System.out.println("将edits log写入到磁盘");
            }
            synBuffer.clear();
        }


    }
}
